#
#  LMusicPodManager.py
#
# This work is released under the GNU GPL, version 2 or later.
#
#
# Implements a queue of operations applied to the currently mounted iPod device
#

from qt import *
from kdecore import *
from utils import *
import time

#
# Event posted from the varous threads to the main iPod manager object
#
class LMusicPodEvent(QCustomEvent):
	def __init__(self,status = None):
		QCustomEvent.__init__(self,QEvent.User+43)
		self._playerStatus = status
	
	def status(self):
		return self._playerStatus

#
# base class for all of the various iPod management thread types
#
class LMusicPodThread(QThread):
	def __init__(self):
		QThread.__init__(self)
		self.lastTime = 0

	#
	# emit a status packet to the UI thread
	#
	def emitEvent(self,status):
		event = LMusicPodEvent(status)
		QApplication.postEvent(LMusicPodManager.singleton(),event)

	#
	# throttle events to 5/sec max
	#
	def maybeEmitEvent(self,status):
		now = long(time.time()*5)
		if now!=self.lastTime:
			self.emitEvent(status)
			self.lastTime = now
	
	#
	# if we were given the name of the playlist, convert it to the actual playlist
	#
	def ensurePlaylist(self,playlist):
		if type(playlist)==unicode or type(playlist)==str:
			from PodLibrary import PodLibrary
			playlist = PodLibrary.singleton().playlistWithName(playlist)
		return playlist

#
# set the owner name
#
class LMusicPodSetOwnerNameThread(LMusicPodThread):
	def __init__(self,name):
		LMusicPodThread.__init__(self)
		from PodLibrary import PodLibrary
		self.library = PodLibrary.singleton()
		self.name = name
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Setting Owner Name','Name':self.name})
		self.library.setOwnerName(self.name)
		self.emitEvent({'Source':'POD','Status':'Set Owner Name','Name':self.name})

#
# add tracks to playlist
#
class LMusicPodAddTrackIDsToPlaylistThread(LMusicPodThread):
	def __init__(self,playlist,trackIDs):
		LMusicPodThread.__init__(self)
		self.playlist = self.ensurePlaylist(playlist)
		self.trackIDs = trackIDs
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Adding TrackIDs','Playlist':self.playlist,'TrackIDs':self.trackIDs})
		if self.playlist: self.playlist.addPodTrackIDs(self.trackIDs)
		self.emitEvent({'Source':'POD','Status':'Added TrackIDs','Playlist':self.playlist,'TrackIDs':self.trackIDs})

#
# remove a track from a playlist - if the playlist is the master list
# then the track is removed from all playlists and erased from the device
#
class LMusicPodRemoveTrackIDsFromPlaylistThread(LMusicPodThread):
	def __init__(self,playlist,trackIDs):
		LMusicPodThread.__init__(self)
		self.playlist = self.ensurePlaylist(playlist)
		self.trackIDs = trackIDs
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Removing TrackIDs','Playlist':self.playlist,'TrackIDs':self.trackIDs})
		if self.playlist: self.playlist.removePodTrackIDs(self.trackIDs)
		self.emitEvent({'Source':'POD','Status':'Removed TrackIDs','Playlist':self.playlist,'TrackIDs':self.trackIDs})

#
# add a new playlist to the device
#
class LMusicPodAddNewPlaylistThread(LMusicPodThread):
	def __init__(self,name):
		LMusicPodThread.__init__(self)
		from PodLibrary import PodLibrary
		self.library = PodLibrary.singleton()
		self.name = name
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Adding Playlist','Name':self.name})
		playlist = self.library.playlistWithName(self.name)
		if playlist == None: self.library.addNewPlaylist(self.name)
		self.emitEvent({'Source':'POD','Status':'Added Playlist','Name':self.name})

#
# remove a playlist from the device
#
class LMusicPodRemovePlaylistThread(LMusicPodThread):
	def __init__(self,playlist):
		LMusicPodThread.__init__(self)
		self.playlist = self.ensurePlaylist(playlist)
		self.library = self.playlist.library()
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Removing Playlist','Playlist':self.playlist})
		if self.playlist: self.library.removePlaylist(self.playlist)
		self.emitEvent({'Source':'POD','Status':'Removed Playlist','Playlist':self.playlist})

#
# change the name of a playlist
#
class LMusicPodRenamePlaylistThread(LMusicPodThread):
	def __init__(self,playlist,name):
		LMusicPodThread.__init__(self)
		self.playlist = self.ensurePlaylist(playlist)
		self.name = name
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Renaming Playlist','Playlist':self.playlist,'Name':self.name})
		if self.playlist: self.playlist.setName(self.name)
		self.emitEvent({'Source':'POD','Status':'Renamed Playlist','Playlist':self.playlist,'Name':self.name})

def uploadCallback(sent,total,item):
	item.callback(sent,total)

#
# upload a track to the device
#
class LMusicPodUploadTrackToPlaylistThread(LMusicPodThread):
	def __init__(self,playlist,track):
		LMusicPodThread.__init__(self)
		self.playlist = self.ensurePlaylist(playlist)
		self.track = track # note this is a mainLibrary track
	
	def callback(self,sent,total):
		self.maybeEmitEvent({'Source':'POD','Status':'Uploading','Playlist':self.playlist,'Track':self.track,'currentTime':sent,'totalTime':total})

	def run(self):
		if not self.playlist.library().diskFull():
			self.emitEvent({'Source':'POD','Status':'Uploading Track','Playlist':self.playlist,'Track':self.track})
			if self.playlist: self.playlist.uploadTrack(self.track,uploadCallback,self)
			self.emitEvent({'Source':'POD','Status':'Uploaded Track','Playlist':self.playlist,'Track':self.track})
		else:
			self.emitEvent({'Source':'POD','Status':'Disk Full'})

def downloadCallback(sent,total,item):
	item.callback(sent,total)

#
# download a track from the device
#
class LMusicPodDownloadTrackToPlaylistThread(LMusicPodThread):
	def __init__(self,playlist,track):
		LMusicPodThread.__init__(self)
		self.playlist = playlist # note this is a mainLibrary playlist
		self.track = track
	
	def callback(self,sent,total):
		self.maybeEmitEvent({'Source':'POD','Status':'Downloading','Playlist':self.playlist,'Track':self.track,'currentTime':sent,'totalTime':total})

	def run(self):
		self.emitEvent({'Source':'POD','Status':'Downloading Track','Playlist':self.playlist,'Track':self.track})
		filePath = self.track.download(downloadCallback,self)
		self.emitEvent({'Source':'POD','Status':'Downloaded Track','Playlist':self.playlist,'Track':self.track,'filePath':filePath})

#
# write the library to the iPod if dirty
#
class LMusicPodUpdateThread(LMusicPodThread):
	def __init__(self):
		LMusicPodThread.__init__(self)
		from PodLibrary import PodLibrary
		self.library = PodLibrary.singleton()
	
	def run(self):
		self.emitEvent({'Source':'POD','Status':'Updating'})
		self.library.writeStuff()
		self.emitEvent({'Source':'POD','Status':'Updated'})

def gRefreshCallback(status,index,total,object):
	object.refreshCallback(status,index,total)

class LMusicPodManager(QObject):
	def __init__(self):
		QObject.__init__(self)
		self.actions = []
		self.process = None
		self.altProcess = None # keeps reference to avoid premature GC of process
		from LApplication import LApplication
		self.app = LApplication.singleton()
	
	def refreshCallback(self,status,index,total):
		try:
			self.progress.setLabelText(status)
			self.progress.setTotalSteps(total)
			self.progress.setProgress(index)
			self.app.processEvents()
		except: pass

	def doRefresh(self):
		#print "refreshing"
		from PodLibrary import PodLibrary
		self.progress = QProgressDialog(None,"progress",True)
		self.progress.setMinimumDuration(0)
		self.progress.setCaption(i18n("Refreshing"));
		PodLibrary.singleton().readMusic(gRefreshCallback,self) # do this synchronous to avoid threadwars
		self.progress = None
	
	def processNextItem(self):
		if self.process==None or not self.process.running():
			if len(self.actions)>0:
				action = self.actions[0]
				self.actions = self.actions[1:]
				self.process = None
				type = action['Action']
				if type=='Refresh':
					#self.process = LMusicPodRefreshThread()
					self.doRefresh()
					self.processNextItem()
				elif type=='AddNewHostPlaylist':
					from Library import Library
					lib = Library.mainLibrary()
					name = action['Name']
					if lib.playlistWithName(name)==None:
						lib.addNewPlaylist(name) # also synchronous to avoid thread problems
					self.processNextItem()
				elif type=='Sync1':
					self.sync1() # synchronously sets up async actions
					self.processNextItem()
				elif type=='Sync2':
					self.sync2() # synchronously sets up async actions
					self.processNextItem()
				else:
					if type=='SetOwnerName':
						self.process = LMusicPodSetOwnerNameThread(action['Name'])
					elif type=='AddTrackIDsToPlaylist':
						self.process = LMusicPodAddTrackIDsToPlaylistThread(action['Playlist'],action['TrackIDs'])
					elif type=='RemoveTrackIDsFromPlaylist':
						self.process = LMusicPodRemoveTrackIDsFromPlaylistThread(action['Playlist'],action['TrackIDs'])
					elif type=='AddNewPlaylist':
						self.process = LMusicPodAddNewPlaylistThread(action['Name'])
					elif type=='RemovePlaylist':
						self.process = LMusicPodRemovePlaylistThread(action['Playlist'])
					elif type=='RenamePlaylist':
						self.process = LMusicPodRenamePlaylistThread(action['Playlist'],action['Name'])
					elif type=='UploadTrackToPlaylist':
						self.process = LMusicPodUploadTrackToPlaylistThread(action['Playlist'],action['Track'])
					elif type=='DownloadTrackToPlaylist':
						self.process = LMusicPodDownloadTrackToPlaylistThread(action['Playlist'],action['Track'])
					elif type=='Update':
						self.process = LMusicPodUpdateThread()
					else:
						print "LMusicPodManager: unknown action",action
					if self.process:
						#print "LMusicPodManager starting thread",self.process
						self.process.start()
			else:
				#print "LMusicPodManager no actions"
				pass
		else:
			#print "LMusicPodManager - busy"
			pass
		
	def customEvent(self,e):
		status = e.status()
		if status:
			#print "LMusicPodManager.customEvent()",status
			self.emit(PYSIGNAL('status'),(status,None))
			s = status['Status']
			if s=='Disk Full':
				# remove any pending Uploads in the queue
				self.actions = [action for action in self.actions if action['Action']!='UploadTrackToPlaylist']
			if s in ['Removed TrackIDs','Added TrackIDs','Added Playlist','Removed Playlist','Renamed Playlist','Uploaded Track','Downloaded Track','Updated','Refreshed','Disk Full']:
				self.altProcess = self.process # keep reference to keep GC happy
				self.process = None
				if len(self.actions)==0:
					if s=='Updated':
						self.actions.append({'Action':'Refresh'}) # if just updated, then refresh
					elif not s in ['Refreshed','Downloaded Track']:
						self.actions.append({'Action':'Update'}) # if might be dirty, then cause update
				self.processNextItem()
	
	def setOwnerName(self,name):
		self.actions.append({'Action':'SetOwnerName','Name':unikode(name)})
		self.processNextItem()
	
	def addTrackIDsToPlaylist(self,playlist,trackIDs):
		self.actions.append({'Action':'AddTrackIDsToPlaylist','Playlist':playlist,'TrackIDs':trackIDs})
		self.processNextItem()
	
	def removeTrackIDsFromPlaylist(self,playlist,trackIDs):
		self.actions.append({'Action':'RemoveTrackIDsFromPlaylist','Playlist':playlist,'TrackIDs':trackIDs})
		self.processNextItem()

	def addNewPlaylist(self,name):
		self.actions.append({'Action':'AddNewPlaylist','Name':unikode(name)})
		self.processNextItem()

	def addNewPlaylists(self,names):
		for name in names:
			self.actions.append({'Action':'AddNewPlaylist','Name':unikode(name)})
		self.processNextItem()
	
	def removePlaylist(self,playlist):
		self.actions.append({'Action':'RemovePlaylist','Playlist':playlist})
		self.processNextItem()

	def removePlaylists(self,playlists):
		for playlist in playlists:
			self.actions.append({'Action':'RemovePlaylist','Playlist':playlist})
		self.processNextItem()

	def renamePlaylist(self,playlist,name):
		self.actions.append({'Action':'RenamePlaylist','Playlist':playlist,'Name':unikode(name)})
		self.processNextItem()
	
	def uploadTrackToPlaylist(self,playlist,track):
		self.actions.append({'Action':'UploadTrackToPlaylist','Playlist':playlist,'Track':track})
		self.processNextItem()

	def uploadTracksToPlaylist(self,playlist,tracks):
		for track in tracks:
			self.actions.append({'Action':'UploadTrackToPlaylist','Playlist':playlist,'Track':track})
		self.processNextItem()

	def downloadTrackToPlaylist(self,playlist,track):
		self.actions.append({'Action':'DownloadTrackToPlaylist','Playlist':playlist,'Track':track})
		self.processNextItem()

	def downloadTracksToPlaylist(self,playlist,tracks):
		#print "downloading",tracks,"to",playlist
		for track in tracks:
			self.actions.append({'Action':'DownloadTrackToPlaylist','Playlist':playlist,'Track':track})
		self.processNextItem()

	def addNewHostPlaylist(self,name):
		self.actions.append({'Action':'AddNewHostPlaylist','Name':unikode(name)})
		self.processNextItem()

	def addNewHostPlaylists(self,names):
		for name in names:
			self.actions.append({'Action':'AddNewHostPlaylist','Name':unikode(name)})
		self.processNextItem()
	
	#
	# sync is split up into two parts in order for the player to stabilize its playlists -
	# it's tough to figure out how to add tracks to the playlist until it actually exists.
	#
	def sync(self):
		self.actions.append({'Action':'Sync1'})
		self.processNextItem()

	def sync1(self):
		#print "LMusicPodManager: sync phase 1"
		from PodLibrary import PodLibrary
		from Library import Library
		from LSettings import LSettings
		pod = PodLibrary.singleton()
		podMain = pod.mainPlaylist
		library = Library.mainLibrary()
		settings = LSettings.settings()
		progress = QProgressDialog(None,"progress",True)
		progress.setMinimumDuration(0)
		progress.setCaption(i18n("Syncing"))
		progress.setTotalSteps(3)
		progress.setProgress(1)
		musicMode = settings.get("Player Music Sync","union")
		playlistMode = settings.get("Player Playlist Sync","union")
		progress.forceShow()
		self.app.processEvents()
		(tracksAdd,tracksRemove,playlistsAdd,playlistsRemove) = pod.getSyncInfoWith(library)
		#
		# add or remove playlists per settings
		#
		progress.setLabelText(i18n("Syncing playlists"))
		progress.setProgress(2)
		self.app.processEvents()
		modified = False
		if playlistMode=='sync':
			if len(playlistsRemove):
				self.removePlaylists(playlistsRemove.keys())
				modified = True
		elif playlistMode=='union':
			if len(playlistsRemove):
				self.addNewHostPlaylists(playlistsRemove.keys())
				modified = True
		#print "creating Pod playlists",playlistsAdd.keys()
		if len(playlistsAdd):
			self.addNewPlaylists(playlistsAdd.keys())
			modified = True
		#
		# copy or remove tracks per settings
		#
		progress.setLabelText(i18n("Syncing tracks"))
		progress.setProgress(3)
		self.app.processEvents()
		if musicMode=='sync':
			if len(tracksRemove):
				self.removeTracks(tracksRemove.values())
				modified = True
		elif musicMode=='union':
			if len(tracksRemove):
				self.downloadTracksToPlaylist(None,tracksRemove.values())
				modified = True
		if len(tracksAdd):
			self.uploadTracksToPlaylist(podMain,tracksAdd.values())
			modified = True
		if modified:
			self.update()
			self.refresh()
		self.actions.append({'Action':'Sync2'})
	
		#
		# now make sure all the tracks are in the proper playlists
		#
	def sync2(self):
		#print "LMusicPodManager: sync phase 2"
		from PodLibrary import PodLibrary
		from Library import Library
		from LSettings import LSettings
		pod = PodLibrary.singleton()
		podMain = pod.mainPlaylist
		library = Library.mainLibrary()
		settings = LSettings.settings()
		musicMode = settings.get("Player Music Sync","union")
		playlistMode = settings.get("Player Playlist Sync","union")
		progress = QProgressDialog(None,"progress",True)
		progress.setMinimumDuration(0)
		progress.setCaption(i18n("Fixing playlists"))
		hostHashes = library.hashList()
		hostPlaylists = library.getPlaylists()
		progress.setTotalSteps(len(hostPlaylists))
		progress.forceShow()
		self.app.processEvents()
		index = 0
		modified = False
		for hostPlaylist in hostPlaylists:
			index = index+1
			progress.setProgress(index)
			progress.setLabelText(hostPlaylist.name)
			self.app.processEvents()
			if not hostPlaylist.master and not hostPlaylist.trash:
				podPlaylist = pod.playlistWithName(hostPlaylist.name)
				if podPlaylist:
					(tracksAdd,tracksRemove) = podPlaylist.getSyncInfoWith(hostPlaylist)
					if musicMode=='sync':
						if len(tracksRemove):
							self.removeTracksFromPlaylist(podPlaylist,tracksRemove.values())
							modified = True
					if musicMode=='union' or musicMode=='add':
						if len(tracksAdd):
							self.uploadTracksToPlaylist(podPlaylist,tracksAdd.values())
							modified = True
					if musicMode=='union':
						for trackKey in tracksRemove.keys():
							track = hostHashes.get(trackKey,None)
							if track:
								hostPlaylist.addTrack(track)
								modified = True
		if modified:
			self.update()

	def update(self):
		self.actions.append({'Action':'Update'})
		self.processNextItem()
	
	def refresh(self):
		self.actions.append({'Action':'Refresh'})
		self.processNextItem()
	
	def cancel(self):
		self.actions = []
		self.update() # force any changes done so far to the player

	def static_singleton():
		global _podManagerSingleton
		if _podManagerSingleton==None:
			_podManagerSingleton = LMusicPodManager()
		return _podManagerSingleton
	singleton = staticmethod(static_singleton)

_podManagerSingleton = None
